package uk.co.q3c.deplan.client.domain.resource;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;

import javax.persistence.Transient;

import uk.co.q3c.deplan.client.domain.DomainKind;
import uk.co.q3c.deplan.client.domain.task.BaseTask;
import uk.co.q3c.deplan.client.domain.task.StartFinishTimes;
import uk.co.q3c.deplan.client.service.ServiceFactory;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Cached;
import com.googlecode.objectify.annotation.Unindexed;

/**
 * @see ResourcePool
 * @author David Sowerby 7 Aug 2010
 * 
 */
@Unindexed
@Cached(expirationSeconds = 600)
public class ResourceGroup extends AbstractResource implements Serializable {
	protected static final Logger logger = Logger.getLogger(ResourceGroup.class.getName());
	private static final long serialVersionUID = 1L;

	private List<Key<IndividualResource>> individualsKeys = new ArrayList<Key<IndividualResource>>();

	@Transient
	private List<IndividualResource> individuals;

	private Key<ResourceGroup> key;

	boolean proportionalBooking;

	protected ResourceGroup() {
		super();
	}

	public ResourceGroup(ServiceFactory connection) {
		super(connection);

	}

	/**
	 * Get a copied list of resources. The resources are references to the originals.
	 * 
	 * @see uk.co.q3c.deplan.client.domain.resource.Resource#getResources()
	 */
	@Override
	public List<IndividualResource> getResources() {
		ArrayList<IndividualResource> copy = new ArrayList<IndividualResource>();
		copy.addAll(resources());
		return copy;
	}

	public void setResourcesKeys(List<Key<IndividualResource>> resources) {
		clearResources();
		for (Key<IndividualResource> resource : resources) {
			addResource(resource);
		}
	}

	public void setResources(List<IndividualResource> resources) {
		clearResources();
		individualsKeys = keysfromResources(resources);
		individuals = resources;
	}

	private List<Key<IndividualResource>> keysfromResources(List<IndividualResource> resources) {
		List<Key<IndividualResource>> keys = new ArrayList<Key<IndividualResource>>();
		for (IndividualResource resource : resources) {
			try {
				keys.add(resource.getKey());
			} catch (PersistenceReferenceException e) {
				// just ignore - if it has not been saved it just won't have a key
			}
		}
		return keys;
	}

	public void addResource(Key<IndividualResource> resource) {
		individualsKeys.add(resource);
		individuals = null; // now out of date
	}

	public void addResource(IndividualResource resource) throws PersistenceReferenceException {
		resources().add(resource);
		individualsKeys.add(resource.getKey());

	}

	public void clearResources() {
		individualsKeys.clear();
		individuals = null; // now out of date
	}

	public void removeResource(IndividualResource resource) {
		resources().remove(resource);
		try {
			individualsKeys.remove(resource.getKey());
		} catch (PersistenceReferenceException e) {
			// just ignore, it cannot be in the keys list
		}
		fireStructureChange(resource, false);
	}

	/**
	 * Books the amount of effort given across all contained resources according to the setting of {@link #setProportionalBooking(boolean)}.
	 * When {@link IndividualResource#bookEffort()} is called with the appropriate booking, the associated task is also updated.
	 * 
	 * @param task
	 *            the task for which the time is being booked
	 * @param di
	 *            the date of the booking
	 * @param effortWanted
	 *            the amount of effort in minutes that is required
	 * @return the start and plannedFinish times of the booking
	 * @throws PersistenceReferenceException
	 * @see #setProportionalBooking(boolean)
	 */
	// @Override
	public StartFinishTimes bookEffort(int effortWanted, Date di, BaseTask task) throws PersistenceReferenceException {
		StartFinishTimes sft = null;
		if (resources().size() <= 0) {
			logger.warning("call to bookEffort with no resources defined");
			return null;
		}
		ArrayList<Resource> sortedList = new ArrayList<Resource>();
		int effortToBook = effortWanted < effortAvailable(di, 100) ? effortWanted : effortAvailable(di, 100);

		// copy resources using an insertion sort, sorted on effortAvailable,
		// ascending
		for (Resource resource : resources()) {
			int effortAvail = resource.effortAvailable(di, 100);
			int i = 0;
			boolean notInserted = true;
			while (i < sortedList.size() && notInserted) {

				if (effortAvail >= sortedList.get(i)
						.effortAvailable(di, 100)) {
					sortedList.add(i + 1, resource);
					notInserted = false;
				}

				i++;
			}
			if (notInserted) {
				sortedList.add(resource);
			}
		}

		// now work out how to apportion booking

		// sft has start and plannedFinish set so comparison works later (see sftTemp)
		sft = new StartFinishTimes();
		sft.start = Long.MAX_VALUE;
		sft.finish = 0;

		if (isProportionalBooking()) {
			StartFinishTimes sftTemp;
			int remainingBooking = effortToBook;
			// use 1.0 (as opposed to 1) to force double
			double minChunk = 1.0 / effortAvailable(di, 100);
			for (int i = 0; i < sortedList.size(); i++) {
				// if this is the last resource pick up all remaining work, to
				// make
				// sure
				// nothing gets lost through rounding
				if (i == sortedList.size() - 1) {
					if (remainingBooking > 0) {
						sftTemp = sortedList.get(i)
								.bookEffort(remainingBooking, di, task);
						adjustSft(sft, sftTemp);
					}
				} else {
					int toBook = (int) (sortedList.get(i)
							.effortAvailable(di, 100) * minChunk * effortWanted);
					if (toBook > 0) {
						sftTemp = sortedList.get(i)
								.bookEffort(toBook, di, task);
						adjustSft(sft, sftTemp);
						// this is what's left to book
						remainingBooking -= toBook;
					}
				}
			}

			// equal distribution
		} else {
			// remaining starts off as the total

			int remainingBooking = effortWanted;
			for (int i = 0; i < sortedList.size(); i++) {
				// reduce the divisor by one each time round
				int averageBooking = remainingBooking / (sortedList.size() - i);
				int available = sortedList.get(i)
						.effortAvailable(di, 100);
				// use whichever is lower, the amount wanted, or the amount
				// available
				int toBook = available < averageBooking ? available : averageBooking;
				// book it Danno
				if (toBook > 0) {
					StartFinishTimes sftTemp = sortedList.get(i)
							.bookEffort(toBook, di, task);
					adjustSft(sft, sftTemp);
					// this is what's left to book
					remainingBooking -= toBook;
				}
			}
		}
		return sft;
	}

	private void adjustSft(StartFinishTimes sft, StartFinishTimes sftTemp) {
		if (sftTemp.start < sft.start) {
			sft.start = sftTemp.start;
		}
		if (sftTemp.finish > sft.finish) {
			sft.finish = sftTemp.finish;
		}

	}

	// @Override
	/**
	 * Returns the total effort available for all contained resources
	 * 
	 * @param di
	 *            the date
	 * @param percentage
	 *            the level of effort to be used
	 */
	public int effortAvailable(Date di, double percentage) {
		int effort = 0;
		for (Resource resource : resources()) {
			effort += resource.effortAvailable(di, percentage);
		}
		return effort;
	}

	@Override
	public int timeBooked(Date date) {
		int timebooked = 0;
		for (Resource resource : resources()) {
			timebooked += resource.timeBooked(date);
		}
		return timebooked;
	}

	/**
	 * Adds the supplied calendar adjustment to all contained resources
	 * 
	 * @param di
	 *            the date the adjustment is to be effective
	 * @param absolute
	 *            if true number of minutes is absolute, else it is a relative adjustment
	 * @param minutes
	 *            how much adjustment to make
	 */
	public void addAdjustment(Date date, boolean absolute, int minutes) {
		for (Resource resource : resources()) {
			resource.addAdjustment(date, absolute, minutes);
		}
	}

	public boolean isProportionalBooking() {

		return proportionalBooking;
	}

	/**
	 * If true, calls to {@link #bookEffort} have the effort spread in proportion to the time available for each resource. If false, the
	 * effort is spread evenly across all resources, unless a resource has insufficient capacity, in which case the overflow is distributed
	 * evenly among the other resources.
	 * 
	 * @param proportionalBooking
	 */
	public void setProportionalBooking(boolean proportionalBooking) {

		boolean oldValue = this.proportionalBooking;
		this.proportionalBooking = proportionalBooking;
		firePropertyChange("proportionalBooking", oldValue, this.proportionalBooking);

	}

	/**
	 * Loads resources from keys if not already loaded
	 */

	@Override
	public List<IndividualResource> resources() {
		if (individuals == null) {
			individuals = connection.resourceService()
					.individualsFromKeys(individualsKeys);
		}
		return individuals;
	}

	// @Override
	public boolean isGroup() {
		return true;
	}

	public boolean contains(IndividualResource ir) {
		try {
			return individualsKeys.contains(ir.getKey());
		} catch (PersistenceReferenceException e) {
			return false;
		}
	}

	/**
	 * Returns true if this group contains all the resources in <code>res</code> . It may however contain further additional resources. To
	 * ensure a perfect match use {@link #containsAllAndOnly(IndividualResource[])}
	 * 
	 */
	public boolean containsAllKeys(List<Key<IndividualResource>> res) {
		boolean present = true;
		for (int i = 0; i < res.size(); i++) {
			try {
				present = individualsKeys.contains(res.get(i));
			} catch (Exception e) {
				String msg = "Resource reference is null";
				logger.warning(msg);
				throw new InvalidResourceException(msg);
			}
			if (!present) {
				break;
			}
		}
		return present;
	}

	/**
	 * Returns true if this group contains all the resources in <code>res</code> . It may however contain further additional resources. To
	 * ensure a perfect match use {@link #containsAllAndOnly(IndividualResource[])}
	 * 
	 */
	public boolean containsAll(List<IndividualResource> res) {
		List<Key<IndividualResource>> r = keysfromResources(res);
		return containsAllKeys(r);
	}

	/**
	 * returns true if this group contains resources with the all of the <code>names</code>. Normally this should be the same as using
	 * {@link #containsAll(IndividualResource[])} but during an import there may be situations where there are resources with the same name,
	 * but not genuinely the same. There may be further additional resources in this group. If an exact match is needed use
	 * {@link #containsAllAndOnly(String[])}
	 * 
	 * @param names
	 * @return
	 */
	public boolean containsAll(String[] names) {
		boolean present = true;
		for (int i = 0; i < names.length; i++) {
			present = namesMap().containsKey(names[i]);
			if (!present) {
				break;
			}
		}
		return present;
	}

	private Map<String, IndividualResource> namesMap() {
		Map<String, IndividualResource> map = new TreeMap<String, IndividualResource>();
		List<IndividualResource> resources = resources();
		for (IndividualResource resource : resources) {
			map.put(resource.getName(), resource);
		}
		return map;
	}

	/**
	 * Returns true this group contains all the resources in <code>res</code> and only those resources. If it isn't important if there are
	 * additional resources in this group, use {@link #containsAll(IndividualResource[])}
	 * 
	 * @param res
	 * @return
	 */
	public boolean containsAllAndOnlyKeys(List<Key<IndividualResource>> keys) {
		// reject if request has different number of resources - easy check
		boolean result = keys.size() == resources().size();
		// if it has, make sure they are the right ones
		if (result) {
			return containsAllKeys(keys);
		}
		return result;
	}

	/**
	 * Returns true this group contains all the resources in <code>res</code> and only those resources. If it isn't important if there are
	 * additional resources in this group, use {@link #containsAll(IndividualResource[])}
	 * 
	 * @param res
	 * @return
	 */
	public boolean containsAllAndOnly(List<IndividualResource> resources) {
		return containsAllAndOnlyKeys(keysfromResources(resources));
	}

	/**
	 * returns true if this group contains resources with the all of the <code>names</code>. Normally this should be the same as using
	 * {@link #containsAll(IndividualResource[])} but during an import there may be situations where there are resources with the same name,
	 * but not actually genuinely the same. This will return true only if this group has no further resources. For a less exact match use
	 * {@link #containsAll(String[])}
	 * 
	 * @param names
	 * @return
	 */
	public boolean containsAllAndOnly(String[] names) {
		boolean result = names.length == resources().size();
		if (result) {
			return containsAll(names);
		}
		return result;
	}

	public IndividualResource getResource(String name) {
		IndividualResource r = null;
		for (IndividualResource r1 : resources()) {
			if (r1.getName() == null && name == null) {
				return r1;
			}
			if (r1.getName()
					.equals(name)) {
				r = r1;
				break;
			}
		}
		return r;
	}

	@Override
	public List<IndividualResource> getIndividualResources(List<IndividualResource> list) {
		list.addAll(resources());
		return list;
	}

	@Override
	public String toString() {
		return getName() + " " + resources().toString();

	}

	public boolean hasResources() {
		return individualsKeys.size() > 0;
	}

	@Override
	public int count() {
		return individualsKeys.size();
	}

	@Override
	public boolean uses(IndividualResource usedResource) {
		try {
			return individualsKeys.contains(usedResource.getKey());
		} catch (PersistenceReferenceException e) {
			return false;
		}
	}

	public int size() {
		return individualsKeys.size();

	}

	@Override
	public DomainKind getKind() {
		return DomainKind.ResourceGroup;
	}

	@Override
	public Key<ResourceGroup> getKey() throws PersistenceReferenceException {
		if (getKeyId() == null) {
			throw new PersistenceReferenceException(this.getName() + " has not been saved and cannot therefore be used as a reference");
		}
		if (key == null) {
			key = connection.resourceService()
					.keyFor(this);
		}
		return key;
	}

	public IndividualResource getResource(Key<IndividualResource> searchKey) {
		for (IndividualResource r : individuals) {
			try {
				if (r.getKey()
						.equals(searchKey)) {
					return r;
				}
			} catch (PersistenceReferenceException e) {
				return null;
			}
		}
		return null;
	}
}
